<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  <title>JSON 对比工具（带字段统计）</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      padding: 20px;
      background-color: #f4f6f8;
    }
    h1 {
      text-align: center;
      color: #333;
    }
    .container {
      /*max-width: 1200px;*/
      margin: 0 auto;
      background: white;
      padding: 20px;
      border-radius: 8px;
      box-shadow: 0 2px 10px rgba(0,0,0,0.1);
    }
    textarea {
      width: 98%;
      height: 300px;
      padding: 10px;
      margin-bottom: 10px;
      border: 1px solid #ccc;
      border-radius: 4px;
      resize: vertical;
      font-family: Consolas, monospace;
      font-size: 14px;
      background-color: #fff;
    }
    .btn {
      padding: 10px 15px;
      background-color: #007BFF;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      font-size: 14px;
      margin-right: 8px;
      margin-bottom: 5px;
    }
    .btn:hover {
      background-color: #0056b3;
    }
	.btn-format      { background-color: #5cb85c; }
    .btn-sort        { background-color: #f0ad4e; }
    .btn-deep-sort   { background-color: #e67e22; }
    .btn-count       { background-color: #9b59b6; }
    .btn-compress    { background-color: #34495e; color: #fff; }
    .btn-escape      { background-color: #17a2b8; }
    .btn-unescape    { background-color: #17a2b8; }
    .btn-unicode     { background-color: #8e44ad; color: #fff; }
    .btn-chinese     { background-color: #c0392b; color: #fff; }
    .btn-compare     { background-color: #007BFF; font-size: 16px; padding: 10px 20px; }
    .result {
      margin-top: 20px;
      border: 1px solid #ddd;
      padding: 15px;
      border-radius: 4px;
      background-color: #f9f9f9;
      white-space: pre-wrap;
      font-family: Consolas, monospace;
      font-size: 14px;
      line-height: 1.5;
    }
    .missing {
      color: #d9534f;
    }
    .extra {
      color: #5cb85c;
    }
    .field-link {
      color: #007BFF;
      text-decoration: underline;
      cursor: pointer;
      font-weight: bold;
    }
    .field-link:hover {
      opacity: 0.8;
    }
    .comparison-grid {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 15px;
      margin-top: 15px;
    }
    .label {
      font-weight: bold;
      margin-bottom: 5px;
      color: #333;
      display: flex;
      justify-content: space-between;
      align-items: center;
      flex-wrap: wrap;
      gap: 8px;
    }
    .error {
      color: red;
    }
    .highlight-blue {
      background-color: #d0e6ff;
    }
    /* 统计信息样式 */
    .stats-row {
      display: flex;
      gap: 10px;
      margin-top: 8px;
      font-size: 14px;
      color: #333;
    }
    .stats-item {
      padding: 6px 10px;
      background-color: #f0f4f8;
      border-radius: 4px;
      flex: 1;
      text-align: center;
    }
    .stats-item strong {
      color: #9b59b6;
    }
	  /* 新增的备注输入框样式 */
    .note-input {
      width: 98%;
      padding: 8px;
      margin-bottom: 5px;
      border: 1px solid #ddd;
      border-radius: 4px;
      font-size: 14px;
    }
    .note-input:focus {
      outline: none;
      border-color: #007BFF;
      box-shadow: 0 0 0 2px rgba(0,123,255,0.25);
    }
  </style>
</head>
<body>
  <div class="container">
    <h1>JSON 对比工具（带字段统计）</h1>

    <div class="comparison-grid">
      <div>
        <div class="label">
          <span>JSON 输入 1</span>
          <div>
            <button class="btn btn-format" onclick="formatJSON('json1')">格式化</button>
            <button class="btn btn-sort" onclick="sortAndFormatJSON('json1')">一级排序</button>
            <button class="btn btn-deep-sort" onclick="deepSortAndFormatJSON('json1')">深度排序</button>
            <button class="btn btn-count" onclick="countFieldsIn('json1')">📊 统计字段</button>
			<button class="btn btn-compress" onclick="compressJSON('json1')">🔽 压缩</button>
            <button class="btn btn-escape" onclick="escapeJSON('json1')">🔁 转义</button>
            <button class="btn btn-unescape" onclick="unescapeJSON('json1')">🔁 去转义</button>
            <button class="btn btn-unicode" onclick="unicodeToChinese('json1')">🌏 Unicode→中文</button>
            <button class="btn btn-chinese" onclick="chineseToUnicode('json1')">🇨🇳 中文→Unicode</button>
          </div>
        </div>
		<input class="note-input" id="note1" placeholder="在此输入JSON1的备注信息...">
        <textarea id="json1" placeholder="请输入第一个 JSON 字符串..."></textarea>
        <!-- JSON1 统计显示 -->
        <div class="stats-row">
          <div class="stats-item">JSON1 字段总数：<strong id="count-json1">?</strong></div>
        </div>
      </div>

      <div>
        <div class="label">
          <span>JSON 输入 2</span>
          <div>
            <button class="btn btn-format" onclick="formatJSON('json2')">格式化</button>
            <button class="btn btn-sort" onclick="sortAndFormatJSON('json2')">一级排序</button>
            <button class="btn btn-deep-sort" onclick="deepSortAndFormatJSON('json2')">深度排序</button>
            <button class="btn btn-count" onclick="countFieldsIn('json2')">📊 统计字段</button>
			<button class="btn btn-compress" onclick="compressJSON('json2')">🔽 压缩</button>
            <button class="btn btn-escape" onclick="escapeJSON('json2')">🔁 转义</button>
            <button class="btn btn-unescape" onclick="unescapeJSON('json2')">🔁 去转义</button>
            <button class="btn btn-unicode" onclick="unicodeToChinese('json2')">🌏 Unicode→中文</button>
            <button class="btn btn-chinese" onclick="chineseToUnicode('json2')">🇨🇳 中文→Unicode</button>
          </div>
        </div>
		<input class="note-input" id="note2" placeholder="在此输入JSON2的备注信息...">
        <textarea id="json2" placeholder="请输入第二个 JSON 字符串..."></textarea>
        <!-- JSON2 统计显示 -->
        <div class="stats-row">
          <div class="stats-item">JSON2 字段总数：<strong id="count-json2">?</strong></div>
        </div>
      </div>
    </div>

    <button class="btn btn-compare" onclick="compareJSON()">🔍 对比 JSON</button>

    <!-- 对比时也显示总数 -->
    <div id="jsonStats" style="margin-top: 15px; padding: 10px; background-color: #f0f4f8; border-radius: 4px; font-size: 14px; color: #333;">
      字段总数统计：JSON1 <strong id="count1">?</strong> 个字段，JSON2 <strong id="count2">?</strong> 个字段
    </div>

    <h3 style="margin-top: 20px;">字段差异对比结果</h3>
    <div id="diffResult" class="result">
      点击"对比 JSON"按钮查看差异。
    </div>
  </div>

  <script>
  
 // 压缩JSON
function compressJSON(textareaId) {
  const textarea = document.getElementById(textareaId);
  try {
    const obj = JSON.parse(textarea.value);
    textarea.value = JSON.stringify(obj);
    document.getElementById('diffResult').innerHTML = `<span style="color: #34495e;">✅ ${textareaId.toUpperCase()} 已压缩。</span>`;
  } catch (e) {
    document.getElementById('diffResult').innerHTML = `<span class="error">❌ 压缩失败：${e.message}</span>`;
  }
}

// JSON转义
function escapeJSON(textareaId) {
  const textarea = document.getElementById(textareaId);
  try {
    let value = textarea.value;
    value = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
    textarea.value = value;
    document.getElementById('diffResult').innerHTML = `<span style="color: #17a2b8;">✅ ${textareaId.toUpperCase()} 已转义。</span>`;
  } catch (e) {
    document.getElementById('diffResult').innerHTML = `<span class="error">❌ 转义失败：${e.message}</span>`;
  }
}

// 去除转义
function unescapeJSON(textareaId) {
  const textarea = document.getElementById(textareaId);
  try {
    let value = textarea.value;
    value = value.replace(/\\n/g, '\n').replace(/\\"/g, '"').replace(/\\\\/g, '\\');
    textarea.value = value;
    document.getElementById('diffResult').innerHTML = `<span style="color: #17a2b8;">✅ ${textareaId.toUpperCase()} 已去转义。</span>`;
  } catch (e) {
    document.getElementById('diffResult').innerHTML = `<span class="error">❌ 去转义失败：${e.message}</span>`;
  }
}

// Unicode转中文
function unicodeToChinese(textareaId) {
  const textarea = document.getElementById(textareaId);
  try {
    const value = textarea.value;
    const decoded = value.replace(/\\u([a-fA-F0-9]{4})/g, (match, hex) => String.fromCharCode(parseInt(hex, 16)));
    textarea.value = decoded;
    document.getElementById('diffResult').innerHTML = `<span style="color: #8e44ad;">✅ Unicode 已转为中文。</span>`;
  } catch (e) {
    document.getElementById('diffResult').innerHTML = `<span class="error">❌ 转换失败：${e.message}</span>`;
  }
}

// 中文转Unicode
function chineseToUnicode(textareaId) {
  const textarea = document.getElementById(textareaId);
  try {
    const value = textarea.value;
    const encoded = value.replace(/[\u4e00-\u9fa5]/g, char => '\\u' + char.charCodeAt(0).toString(16).padStart(4, '0'));
    textarea.value = encoded;
    document.getElementById('diffResult').innerHTML = `<span style="color: #c0392b;">✅ 中文已转为 Unicode。</span>`;
  } catch (e) {
    document.getElementById('diffResult').innerHTML = `<span class="error">❌ 转换失败：${e.message}</span>`;
  }
}
  
    // 通用字段计数函数（递归）
    function countFields(obj) {
      if (!obj || typeof obj !== 'object') return 0;
      let count = 0;
      function walk(node) {
        if (Array.isArray(node)) {
          node.forEach(item => walk(item));
        } else if (typeof node === 'object' && node !== null) {
          Object.keys(node).forEach(key => {
            count++;
            walk(node[key]);
          });
        }
      }
      walk(obj);
      return count;
    }

    // 统计指定 textarea 的字段数
    function countFieldsIn(textareaId) {
      const textarea = document.getElementById(textareaId);
      const resultDiv = document.getElementById('diffResult');
      try {
        const value = textarea.value.trim();
        if (!value) {
          resultDiv.innerHTML = `<span class="error">❌ ${textareaId.toUpperCase()} 输入为空，无法统计。</span>`;
          return;
        }
        const obj = JSON.parse(value);
        const totalCount = countFields(obj);
        // 更新对应显示
        document.getElementById(`count-${textareaId}`).textContent = totalCount;
        resultDiv.innerHTML = `<span style="color: #9b59b6;">✅ ${textareaId.toUpperCase()} 共统计到 <strong>${totalCount}</strong> 个字段。</span>`;
      } catch (e) {
        resultDiv.innerHTML = `<span class="error">❌ 统计失败：${e.message}</span>`;
      }
    }

    // 原有功能：格式化
    function formatJSON(textareaId) {
      const textarea = document.getElementById(textareaId);
      const resultDiv = document.getElementById('diffResult');
      try {
        const value = textarea.value.trim();
        if (!value) {
          resultDiv.innerHTML = '<span class="error">❌ 输入为空，无法格式化。</span>';
          return;
        }
        const obj = JSON.parse(value);
        textarea.value = JSON.stringify(obj, null, 2);
        resultDiv.innerHTML = `<span style="color: #5cb85c;">✅ ${textareaId.toUpperCase()} 已格式化！</span>`;
      } catch (e) {
        resultDiv.innerHTML = `<span class="error">❌ 格式化失败：${e.message}</span>`;
      }
    }

    // 原有功能：一级排序
    function sortAndFormatJSON(textareaId) {
      const textarea = document.getElementById(textareaId);
      const resultDiv = document.getElementById('diffResult');
      try {
        const value = textarea.value.trim();
        if (!value) {
          resultDiv.innerHTML = '<span class="error">❌ 输入为空，无法排序。</span>';
          return;
        }
        const obj = JSON.parse(value);
        const sortedObj = Object.keys(obj)
          .sort()
          .reduce((sorted, key) => {
            sorted[key] = obj[key];
            return sorted;
          }, {});
        textarea.value = JSON.stringify(sortedObj, null, 2);
        resultDiv.innerHTML = `<span style="color: #f0ad4e;">✅ ${textareaId.toUpperCase()} 已一级排序并格式化！</span>`;
      } catch (e) {
        resultDiv.innerHTML = `<span class="error">❌ 处理失败：${e.message}</span>`;
      }
    }

    // 深度排序函数
    function deepSortObject(obj) {
      if (typeof obj !== 'object' || obj === null) return obj;
      if (Array.isArray(obj)) return obj.map(deepSortObject);
      const sortedKeys = Object.keys(obj).sort();
      const result = {};
      for (const key of sortedKeys) {
        result[key] = deepSortObject(obj[key]);
      }
      return result;
    }

    function deepSortAndFormatJSON(textareaId) {
      const textarea = document.getElementById(textareaId);
      const resultDiv = document.getElementById('diffResult');
      try {
        const value = textarea.value.trim();
        if (!value) {
          resultDiv.innerHTML = '<span class="error">❌ 输入为空，无法排序。</span>';
          return;
        }
        const obj = JSON.parse(value);
        const sortedObj = deepSortObject(obj);
        textarea.value = JSON.stringify(sortedObj, null, 2);
        resultDiv.innerHTML = `<span style="color: #e67e22;">✅ ${textareaId.toUpperCase()} 已深度排序并格式化！</span>`;
      } catch (e) {
        resultDiv.innerHTML = `<span class="error">❌ 深度排序失败：${e.message}</span>`;
      }
    }

    // 辅助函数：转义正则
    function escapeRegExp(string) {
      return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    }

    // 辅助：计算滚动位置
    function scrollTopForIndex(textarea, index) {
      const text = textarea.value.substring(0, index);
      const lines = text.split('\n');
      const lineHeight = 18;
      return lineHeight * (lines.length - 1);
    }

    // 定位字段（带高亮）
    function findAndSelectInTextarea(textareaId, fieldName, options = {}) {
      const { findPrevious = false, inPath } = options;
      const textarea = document.getElementById(textareaId);
      const text = textarea.value;

      let match, start, end;

      if (inPath) {
        const pathParts = inPath.split('.');
        let currentText = text;
        let currentIndex = 0;

        for (let i = 0; i < pathParts.length; i++) {
          const key = pathParts[i];
          const regex = new RegExp(`[\\n\\s]*["']?${escapeRegExp(key)}["']?\\s*:`, 'g');
          regex.lastIndex = currentIndex;
          match = regex.exec(currentText);
          if (!match) {
            alert(`⚠️ 在 ${textareaId} 中未找到路径: ${inPath}`);
            return;
          }
          start = match.index;
          currentIndex = start + key.length;
        }

        end = text.indexOf('\n', start);
        if (end === -1) end = text.length;
        else {
          let temp = text.indexOf('\n', end + 1);
          end = temp === -1 ? text.length : temp;
        }

        textarea.focus();
        textarea.setSelectionRange(start, end);
        textarea.scrollTop = scrollTopForIndex(textarea, start);
        textarea.classList.add('highlight-blue');
        setTimeout(() => textarea.classList.remove('highlight-blue'), 1500);
        return;
      }

      const regex = new RegExp(`[\\n\\s]*["']?${escapeRegExp(fieldName)}["']?\\s*:`, 'g');
      match = regex.exec(text);

      if (match) {
        start = match.index;
        end = text.indexOf('\n', start);
        if (end === -1) end = text.length;
        else {
          let temp = text.indexOf('\n', end + 1);
          end = temp === -1 ? text.length : temp;
        }

        textarea.focus();
        textarea.setSelectionRange(start, end);
        textarea.scrollTop = scrollTopForIndex(textarea, start);
        textarea.classList.add('highlight-blue');
        setTimeout(() => textarea.classList.remove('highlight-blue'), 1500);
      } else {
        alert(`⚠️ 在 ${textareaId} 中未找到字段: ${fieldName}`);
      }
    }

    // 格式化值用于显示
    function formatValue(val) {
      if (val === undefined) return '<span style="color:#888">undefined</span>';
      try {
        return JSON.stringify(val, null, 0).replace(/</g, '&lt;').replace(/>/g, '&gt;');
      } catch {
        return String(val);
      }
    }

    // 核心对比函数
    function compareJSON() {
      deepSortAndFormatJSON('json1');
      deepSortAndFormatJSON('json2');

      const json1Str = document.getElementById('json1').value.trim();
      const json2Str = document.getElementById('json2').value.trim();
      const resultDiv = document.getElementById('diffResult');

      if (!json1Str || !json2Str) {
        resultDiv.innerHTML = '<span class="error">❌ 请确保两个 JSON 输入框都不为空。</span>';
        return;
      }

      try {
        const obj1 = JSON.parse(json1Str);
        const obj2 = JSON.parse(json2Str);

        // 统计字段数
        const totalFields1 = countFields(obj1);
        const totalFields2 = countFields(obj2);

        // 更新所有统计显示
        document.getElementById('count-json1').textContent = totalFields1;
        document.getElementById('count-json2').textContent = totalFields2;
        document.getElementById('count1').textContent = totalFields1;
        document.getElementById('count2').textContent = totalFields2;

        // === 开始对比逻辑（保持不变）===
        const differences = [];

        function isObject(obj) {
          return obj !== null && typeof obj === 'object' && !Array.isArray(obj);
        }

        function isArrayOrObject(val) {
          return val !== null && typeof val === 'object';
        }

        function guessKeyField(arr) {
          if (arr.length === 0) return 'index';
          const candidates = ['id', 'ID', 'Id', 'key', 'name', 'title'];
          for (let field of candidates) {
            if (arr[0].hasOwnProperty(field)) return field;
          }
          return 'index';
        }

        function safeStringify(val) {
          try {
            return JSON.stringify(val);
          } catch {
            return String(val);
          }
        }

        function walk(obj1, obj2, path = '') {
          const keys1 = new Set(Object.keys(obj1 || {}));
          const keys2 = new Set(Object.keys(obj2 || {}));

          if (Array.isArray(obj1) && Array.isArray(obj2)) {
            const len = Math.max(obj1.length, obj2.length);
            if (obj1.length > 0 && isObject(obj1[0])) {
              const keyField = guessKeyField(obj1);
              const map1 = new Map(obj1.map(item => {
                const k = item[keyField] !== undefined ? item[keyField] : `__index_${obj1.indexOf(item)}`;
                return [k, item];
              }));
              const map2 = new Map(obj2.map(item => {
                const k = item[keyField] !== undefined ? item[keyField] : `__index_${obj2.indexOf(item)}`;
                return [k, item];
              }));
              const allKeys = new Set([...map1.keys(), ...map2.keys()]);
              allKeys.forEach(key => {
                const item1 = map1.get(key);
                const item2 = map2.get(key);
                const itemPath = `${path}[${key}]`;
                if (!item1) {
                  differences.push({ type: 'extra', path: itemPath, value1: undefined, value2: item2 });
                } else if (!item2) {
                  differences.push({ type: 'missing', path: itemPath, value1: item1, value2: undefined });
                } else {
                  walk(item1, item2, itemPath);
                }
              });
            } else {
              for (let i = 0; i < len; i++) {
                const val1 = obj1[i];
                const val2 = obj2[i];
                const itemPath = `${path}[${i}]`;
                if (val1 === undefined) {
                  differences.push({ type: 'extra', path: itemPath, value1: undefined, value2: val2 });
                } else if (val2 === undefined) {
                  differences.push({ type: 'missing', path: itemPath, value1: val1, value2: undefined });
                } else if (isArrayOrObject(val1) || isArrayOrObject(val2)) {
                  walk(val1, val2, itemPath);
                } else if (safeStringify(val1) !== safeStringify(val2)) {
                  differences.push({ type: 'changed', path: itemPath, value1: val1, value2: val2 });
                }
              }
            }
            return;
          }

          if (isObject(obj1) || isObject(obj2)) {
            const allKeys = new Set([...keys1, ...keys2]);
            allKeys.forEach(key => {
              const val1 = obj1?.[key];
              const val2 = obj2?.[key];
              const currentPath = path ? `${path}.${key}` : key;
              if (val1 === undefined) {
                differences.push({ type: 'extra', path: currentPath, value1: undefined, value2: val2 });
              } else if (val2 === undefined) {
                differences.push({ type: 'missing', path: currentPath, value1: val1, value2: undefined });
              } else if (isArrayOrObject(val1) || isArrayOrObject(val2)) {
                walk(val1, val2, currentPath);
              } else if (safeStringify(val1) !== safeStringify(val2)) {
                differences.push({ type: 'changed', path: currentPath, value1: val1, value2: val2 });
              }
            });
          }
        }

        walk(obj1, obj2);

        // 生成结果
        let diffHtml = '<strong>🔍 深度字段差异分析（点击路径可定位）：</strong><br><br>';
        const missing = differences.filter(d => d.type === 'missing');
        const extra = differences.filter(d => d.type === 'extra');
        const changed = differences.filter(d => d.type === 'changed');

        if (missing.length > 0) {
          diffHtml += `<span class="missing">❌ JSON2 缺少（JSON1 有）:</span><br>`;
          missing.forEach(d => {
            const path = d.path;
            const lastDot = path.lastIndexOf('.');
            const parentPath = lastDot === -1 ? '' : path.slice(0, lastDot);
            const fieldName = path.split('.').pop().replace(/\[\d+\]$/, '[*]');
            diffHtml += `  <span class="field-link" onclick="findAndSelectInTextarea('json1', '${fieldName}', { inPath: '${path}' })">"${path}"</span>: ${formatValue(d.value1)}<br>`;
            if (parentPath) {
              const parentField = parentPath.split('.').pop();
              diffHtml += `  <small>→ 定位到 JSON2 中父字段 <code>"${parentField}"</code></small><br>`;
              diffHtml += `  <span class="field-link" onclick="findAndSelectInTextarea('json2', '${parentField}')">[定位到 JSON2 的 "${parentField}"]</span><br>`;
            } else {
              diffHtml += `  <small>→ 根级字段，无父字段</small><br>`;
            }
            diffHtml += "<br>";
          });
        }

        if (extra.length > 0) {
          diffHtml += `<span class="extra">✅ JSON1 缺少（JSON2 有）:</span><br>`;
          extra.forEach(d => {
            const path = d.path;
            const lastDot = path.lastIndexOf('.');
            const parentPath = lastDot === -1 ? '' : path.slice(0, lastDot);
            const fieldName = path.split('.').pop().replace(/\[\d+\]$/, '[*]');
            diffHtml += `  <span class="field-link" onclick="findAndSelectInTextarea('json2', '${fieldName}', { inPath: '${path}' })">"${path}"</span>: ${formatValue(d.value2)}<br>`;
            if (parentPath) {
              const parentField = parentPath.split('.').pop();
              diffHtml += `  <small>→ 定位到 JSON1 中父字段 <code>"${parentField}"</code></small><br>`;
              diffHtml += `  <span class="field-link" onclick="findAndSelectInTextarea('json1', '${parentField}')">[定位到 JSON1 的 "${parentField}"]</span><br>`;
            } else {
              diffHtml += `  <small>→ 根级字段，无父字段</small><br>`;
            }
            diffHtml += "<br>";
          });
        }

        if (changed.length > 0) {
          diffHtml += `<span style="color: #f0ad4e;">🔶 值不同的字段:</span><br>`;
          changed.forEach(d => {
            const path = d.path;
            const fieldName = path.split('.').pop().replace(/\[\d+\]$/, '[*]');
            diffHtml += `  <strong><span class="field-link" onclick="findAndSelectInTextarea('json1', '${fieldName}', { inPath: '${path}' })">"${path}"</span></strong><br>`;
            diffHtml += `    JSON1 → <span style="color: #d9534f;">${formatValue(d.value1)}</span> <span onclick="findAndSelectInTextarea('json1', '${fieldName}', { inPath: '${path}' })">[定位]</span><br>`;
            diffHtml += `    JSON2 → <span style="color: #5cb85c;">${formatValue(d.value2)}</span> <span onclick="findAndSelectInTextarea('json2', '${fieldName}', { inPath: '${path}' })">[定位]</span><br>`;
            diffHtml += "<br>";
          });
        }

        if (differences.length === 0) {
          diffHtml = "✅ 两个 JSON 完全相同（深度对比）！";
        }

        resultDiv.innerHTML = diffHtml;

      } catch (e) {
        resultDiv.innerHTML = `<span class="error">❌ JSON 解析错误：${e.message}</span>`;
      }
    }
  </script>
</body>
</html>